#include <stdio.h>
#include <math.h>

#define MAX_NUM 100
#define HELLO "hello world"
#define LONG_TEXT "C programming language is invented \
in 1970s."

// #define允许多重替换，即一个宏可以包含另一个宏。
#define TWO 2
#define FOUR TWO *TWO

// 同名的宏可以重复定义，只要定义是相同的，就没有问题。如果定义不同，就会报错。
// 正确
#define FOO "hello"
#define FOO "hello"

// 报错
// #define BAR hello
// #define BAR world

// 带参数的宏
#define SQUARE(X) X *X // 宏SQUARE可以接受一个参数X，替换成X*X。

// 原样替换可能导致意料之外的行为。解决办法就是在定义宏的时候，尽量多使用圆括号，这样可以避免很多意外。
#define SQUARE2(X) (X) * (X)

// 宏的参数也可以是空的。
#define getchar() getc(stdin)

#define MAX(x, y) ((x) > (y) ? (x) : (y))
#define IS_EVEN(n) ((n) % 2 == 0)

// 如果宏的长度过长，可以使用反斜杠（\）折行，将宏写成多行。
#define PRINT_NUMS_TO_PRODUCT(a, b)       \
    {                                     \
        int product = (a) * (b);          \
        for (int i = 0; i < product; i++) \
        {                                 \
            printf("%d\n", i);            \
        }                                 \
    }
// 上面示例中，替换文本放在大括号里面，这是为了创造一个块作用域，避免宏内部的变量污染外部。

// 带参数的宏也可以嵌套，一个宏里面包含另一个宏。
#define QUADP(a, b, c) ((-(b) + sqrt((b) * (b)-4 * (a) * (c))) / (2 * (a)))
#define QUADM(a, b, c) ((-(b)-sqrt((b) * (b)-4 * (a) * (c))) / (2 * (a)))
#define QUAD(a, b, c) QUADP(a, b, c), QUADM(a, b, c)

// #运算符，##运算符
#define STR(x) #x
#define XNAME(n) "x" #n // #n指定参数输出为字符串，再跟前面的字符串结合，最终输出为"x4"。如果不加#，这里实现起来就很麻烦了。

// 如果替换后的文本里面，参数需要跟其他标识符连在一起，组成一个新的标识符，可以使用##运算符。它起到粘合作用，将参数“嵌入”一个标识符之中。
#define MK_ID(n) i##n

// 不定参数的宏
#define X(a, b, ...) (10 * (a) + 20 * (b)), __VA_ARGS__
// 上面示例中，X(a, b, ...)表示X()至少有两个参数，多余的参数使用...表示。在替换文本中，__VA_ARGS__代表多余的参数（每个参数之间使用逗号分隔）。下面是用法示例。
// X(5, 4, 3.14, "Hi!", 12)
// 替换成
// (10*(5) + 20*(4)), 3.14, "Hi!", 12

// __VA_ARGS__前面加上一个#号，可以让输出变成一个字符串。
// #define X(...) #__VA_ARGS__
// printf("%s\n", X(1,2,3));  // Prints "1, 2, 3"

// #undef 指令用来取消已经使用#define定义的宏。
// 有时候想重新定义一个宏，但不确定是否以前定义过，就可以先用#undef取消，然后再定义。因为同名的宏如果两次定义不一样，会报错，而#undef的参数如果是不存在的宏，并不会报错。
#define LIMIT 1000
#undef LIMIT

// GCC 的-U选项可以在命令行取消宏的定义，相当于#undef。
// $ gcc -ULIMIT foo.c
// 上面示例中的-U参数，取消了宏LIMIT，相当于源文件里面的#undef LIMIT。

// #include 指令用于编译时将其他源码文件，加载进入当前文件。它有两种形式
// 形式一
// #include <foo.h> // 加载系统提供的文件

// 形式二
// #include "foo.h" // 加载用户提供的文件

// GCC 编译器的-I参数，也可以用来指定include命令中用户文件的加载路径。
// $ gcc -Iinclude/ -o code code.c
// 上面命令中，-Iinclude/指定从当前目录的include子目录里面，加载用户自己的文件。

// #if...#endif指令用于预处理器的条件判断，满足条件时，内部的行会被编译，否则就被编译器忽略
#if 0
  const double pi = 3.1415; // 不会执行
#endif

// #if...#endif之间还可以加入#else指令，用于指定判断条件不成立时，需要编译的语句。
// #define FOO 1
// #if FOO
// printf("defined\n");
// #else
// printf("not defined\n");
// #endif

// #ifdef...#endif 指令用于判断某个宏是否定义过。
// #ifdef MAVIS
//  #include "foo.h"
//  #define STABLES 1
// #else
//  #include "bar.h"
//  #define STABLES 2
// #endif
// #ifdef指令，等同于#if defined

// #ifndef...#endif指令跟#ifdef...#endif正好相反。它用来判断，如果某个宏没有被定义过，则执行指定的操作
// #ifndef MYHEADER_H
//  #define MYHEADER_H
//  #include "myheader.h"
// #endif
// #ifndef等同于#if !defined。

int main()
{
    printf("%d\n", MAX_NUM);
    printf("%s\n", HELLO);
    printf("%s\n", LONG_TEXT);
    printf("%d\t%d\n", TWO, FOUR);
    printf("%s\n", FOO);
    printf("%d\n", SQUARE(2));
    printf("%d\n", SQUARE(3 + 4));  // 19  最终计算的是 3 + 4 * 3 + 4
    printf("%d\n", SQUARE2(3 + 4)); // 49
    printf("%d\n", MAX(123, 456));
    printf("%d\n", IS_EVEN(5));
    PRINT_NUMS_TO_PRODUCT(2, 3);
    double result[] = {QUAD(1, 2, 1)};
    for (int i = 0; i < 2; i++)
    {
        printf("%f\n", result[i]);
    }

    // 那么，什么时候使用带参数的宏，什么时候使用函数呢？
    // 一般来说，应该首先使用函数，它的功能更强、更容易理解。宏有时候会产生意想不到的替换结果，而且往往只能写成一行，除非对换行符进行转义，但是可读性就变得很差。
    // 宏的优点是相对简单，本质上是字符串替换，不涉及数据类型，不像函数必须定义数据类型。而且，宏将每一处都替换成实际的代码，省掉了函数调用的开销，所以性能会好一些。另外，以前的代码大量使用宏，尤其是简单的数学运算，为了读懂前人的代码，需要对它有所了解。

    // 等同于 printf("%s\n", "3.14159");
    printf("%s\n", STR(3.14159));
    // 输出 x4
    printf("%s\n", XNAME(4));

    // 替换后的文本i1、i2、i3是三个标识符，参数n是标识符的一部分。从这个例子可以看到，##运算符的一个主要用途是批量生成变量名和标识符。
    int MK_ID(1), MK_ID(2), MK_ID(3);
    // 等同于
    // int i1, i2, i3;

    // #if...#endif之间还可以加入#else指令，用于指定判断条件不成立时，需要编译的语句。
#define BAR 1
#if BAR
    printf("defined\n");
#else
    printf("not defined\n");
#endif

    // 如果有多个判断条件，还可以加入#elif命令。

    // #if HAPPY_FACTOR == 0
    //   printf("I'm not happy!\n");
    // #elif HAPPY_FACTOR == 1
    //   printf("I'm just regular\n");
    // #else
    //   printf("I'm extra happy!\n");
    // #endif
    // 没有定义过的宏，等同于0。因此如果UNDEFINED是一个没有定义过的宏，那么 #if UNDEFINED为伪，而 #if !UNDEFINED为真。

    // #if的常见应用就是打开（或关闭）调试模式。

#define DEBUG 1

#if DEBUG
    printf("value of i : %d\n", 11);
    printf("value of j : %d\n", 22);
#endif
    // 上面示例中，通过将DEBUG设为1，就打开了调试模式，可以输出调试信息。

    // GCC 的-D参数可以在编译时指定宏的值，因此可以很方便地打开调试开关。

    //$ gcc -DDEBUG=1 foo.c
    // 上面示例中，-D参数指定宏DEBUG为1，相当于在代码中指定#define DEBUG 1。

    // 预定义宏
    // C 语言提供一些预定义的宏，可以直接使用。
    //__DATE__：编译日期，格式为“Mmm dd yyyy”的字符串（比如 Nov 23 2021）。
    //__TIME__：编译时间，格式为“hh:mm:ss”。
    //__FILE__：当前文件名。
    //__LINE__：当前行号。
    //__func__：当前正在执行的函数名。该预定义宏必须在函数作用域使用。
    //__STDC__：如果被设为1，表示当前编译器遵循 C 标准。
    //__STDC_HOSTED__：如果被设为1，表示当前编译器可以提供完整的标准库；否则被设为0（嵌入式系统的标准库常常是不完整的）。
    //__STDC_VERSION__：编译所使用的 C 语言版本，是一个格式为yyyymmL的长整数，C99 版本为“199901L”，C11 版本为“201112L”，C17 版本为“201710L”。

    printf("This function: %s\n", __func__);
    printf("This file: %s\n", __FILE__);
    printf("This line: %d\n", __LINE__);
    printf("Compiled on: %s %s\n", __DATE__, __TIME__);
    printf("C Version: %ld\n", __STDC_VERSION__);

    // #line 指令用于覆盖预定义宏__LINE__，将其改为自定义的行号。后面的行将从__LINE__的新值开始计数。
    // 将下一行的行号重置为 300
    // #line 300

    // #line还可以改掉预定义宏__FILE__，将其改为自定义的文件名。
    // #line 300 "newfilename"
    // 上面示例中，下一行的行号重置为300，文件名重置为newfilename。

    // #error指令用于让预处理器抛出一个错误，终止编译。
    // #if __STDC_VERSION__ != 201112L
    // #error Not C11
    // #endif

    // #pragma指令用来修改编译器属性
    // 使用 C99 标准
    // #pragma c9x on
    return 0;
}